package com.syncomponents.xml;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import javax.xml.transform.TransformerException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.syncomponents.xml.convert.BytesConverter;
import com.syncomponents.xml.convert.JavaUtilDateConverter;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;

/**
 * <code>XmlTransformer</code> converts xml created by the XmlTransformer transformer in to a
 * java object graph. This transformer uses XStream. Xstream uses some clever tricks so
 * objects that get marshalled to XML do not need to implement any interfaces including
 * Serializable and you don't even need to specify a default constructor.
 * 
 * @author sunkey
 * @date Mar 11, 2014
 */
@Component
public class XmlTransformer {

    private final AtomicReference<XStream> xstream = new AtomicReference<XStream>();
    private volatile String driverClass = XStreamFactory.XSTREAM_XPP_DRIVER;
    private volatile Map<String, Class<?>> aliases = new HashMap<String, Class<?>>();
    private volatile Set<Class<? extends Converter>> converters = new HashSet<Class<? extends Converter>>();

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    public Object toObject(Object src) throws TransformerException {
        Object result = null;
        if (src instanceof byte[]) {
            try {
                Reader xml = new InputStreamReader(new ByteArrayInputStream((byte[]) src));
                result = getXStream().fromXML(xml);
            } catch (Exception e) {
                throw new TransformerException(e);
            }
        } else if (src instanceof InputStream) {
            InputStream input = (InputStream) src;
            try {
                Reader xml = new InputStreamReader(input);
                result = getXStream().fromXML(xml);
            } catch (Exception e) {
                throw new TransformerException(e);
            } finally {
                try {
                    input.close();
                } catch (IOException e) {
                    logger.warn("Exception closing stream: ", e);
                }
            }
        } else if (src instanceof String) {
            result = getXStream().fromXML(src.toString());
        }

        return result;

    }

    public String toXML(Object src) throws TransformerException {
        return getXStream().toXML(src);
    }

    public final XStream getXStream() throws TransformerException {
        XStream instance = xstream.get();
        if (instance == null) {
            try {
                instance = new XStreamFactory(driverClass, aliases, converters).getInstance();
                instance.registerConverter(new JavaUtilDateConverter());
                instance.registerConverter(new BytesConverter());
                if (!xstream.compareAndSet(null, instance)) {
                    instance = xstream.get();
                }
            } catch (Exception e) {
                throw new TransformerException("Unable to initialize XStream", e);
            }
        }

        return instance;
    }

    public String getDriverClass() {
        return driverClass;
    }

    public void setDriverClass(String driverClass) {
        this.driverClass = driverClass;
        // force XStream instance update
        this.xstream.set(null);
    }

    public Map<String, Class<?>> getAliases() {
        return aliases;
    }

    public void setAliases(Map<String, Class<?>> aliases) {
        this.aliases = aliases;
        // force XStream instance update
        this.xstream.set(null);
    }

    public Set<Class<? extends Converter>> getConverters() {
        return converters;
    }

    public void setConverters(Set<Class<? extends Converter>> converters) {
        this.converters = converters;
        // force XStream instance update
        this.xstream.set(null);
    }

    public void addAlias(String alias, Class<?> aClass) {
        aliases.put(alias, aClass);
    }

    public Class<?> removeAlias(String alias) {
        return aliases.remove(alias);
    }

    public void addConverter(Class<? extends Converter> converterClass) {
        converters.add(converterClass);
    }

    public boolean removeAlias(Class<? extends Converter> converterClass) {
        return converters.remove(converterClass);
    }
}
